fix(adopt): 兼容 Codex Desktop 新版会话记录 - #881
Conversation
deepcoldy
left a comment
There was a problem hiding this comment.
首次 Review — REQUEST CHANGES(1 处真实回归,已在本机真实数据上复现)
结论先行
改动的核心意图正确且有价值:Codex Desktop/CLI 0.147 起部分 rollout 只把真实用户输入写进 response_item/message(role=user) 而不再写 event_msg/user_message,旧扫描器取不到标题就把这些其实可 resume 的会话丢弃(#878)。本 PR 加的 response_item fallback 确实能把它们捞回来 —— 我在本机 755 条真实 codex rollout 上跑编译后的代码验证:相比 master 多恢复出 10 条此前被漏掉的真实外部会话(PONG/hi 等)。方向没问题。
但同一条新路径引入了一处回归,必须先修。
🔴 P0 阻断:新 response_item fallback 把 botmux 自己的会话泄漏进 /adopt
现象(真实数据复现): 在本机 755 条真实 rollout 上,对比 master 与本 PR head 的 discoverRolloutSessions() 输出:
| master | 本 PR | |
|---|---|---|
| 发现会话数 | 707 | 755 |
| 相比 master 新浮出 | — | 48 |
| ↳ 其中真实外部会话(修复生效) | — | 10 ✅ |
| ↳ 其中 botmux-origin 会话(泄漏) | — | 38 🔴 |
这 38 条 master 上全部被正确隐藏,本 PR 让它们浮进了 picker,标题就是原始的 <botmux_routing> 你运行在飞书(Lark)话题群中… 整块。这直接违反 /adopt 的既定不变量 —— 代码注释白纸黑字写着:botmux 自己的会话对 picker 隐藏,picker 只用来导入真正外部的会话(它们已可通过话题卡/会话关闭卡 resume,重复导入既冗余又困惑)。
根因:复用的 isBotmuxInjected guard 对真实新版 codex envelope 匹配不到。 真实 envelope(单个 input_text 里)的结构是:
<botmux_routing>…</botmux_routing>
<botmux_builtin_skills>…</botmux_builtin_skills> ← 中间插了这一块
<identity>…</identity>
<session_id>…</session_id>
<user_message>…</user_message> ← part 到这里结束,没有尾部 <sender open_id="ou_…"> footer
于是本该命中的三条正则全部落空:
<botmux_routing>那条要求</botmux_routing>之后(最多隔一个<identity>)紧接<session_id>—— 被中间的<botmux_builtin_skills>打断;</user_message>\s*<(?:sender|session_id|…)邻接模式 —— 该 part 到</user_message>就结束,后面没有 footer;<sender … open_id="ou_…">—— 同理,该 part 无 footer。
我把这个真实 part 单独喂给 isBotmuxInjectedPrompt(),返回 false(完整 envelope 就在这一个 part 里,guard 拿到了完整输入仍漏 —— 所以要修的是 guard 本身,不是 response_item 的分片迭代逻辑)。
为什么 PR 的测试绿着却漏了: PR 新增的 "drops botmux-origin rollouts … response_item" 用例,用的是简化版 envelope(没有 <botmux_builtin_skills>、routing 后直接 <session_id>),恰好命中 guard → 测试过。真实 envelope 命中不了。属于"测试夹具比真实数据干净,给了假绿"。
修复方向(已用真实数据验证可行): 让 <botmux_routing> 结构模式容忍 </botmux_routing> 与 <session_id>/<user_message> 之间插入 <botmux_builtin_skills>/<identity> 等块,即"以 <botmux_routing> 开头 → 出现 </botmux_routing> → 其后出现 <user_message>…</user_message>"。我实测这种起始锚定模式在本机 821/821 条真实泄漏 part 上全部命中,且对两个"仅讨论 botmux XML 的外部 prompt"反例均不误伤。附带好处:master 上经 event_msg 路径泄漏的 152 条同类(同一 guard 不完整根因、非本 PR 引入)也会一并修好。
请同时把回归测试的 envelope 换成真实结构(<botmux_builtin_skills> 插在中间、part 结尾即 </user_message> 无 footer),否则改完 guard 测试仍可能假绿。
🟡 P2 非阻断(质量项):合成上下文跳过列表不全
真实数据里还有两类会作为标题浮出,不在 ROLLOUT_SYNTHETIC_USER_PATTERNS 内:
<codex_internal_context source="goal">…(1 条) —— 疑似 botmux v3 goal-mode 续跑注入,可能也应归为 botmux-origin 排除;<turn_aborted>…(2 条) —— Codex 的中断续跑合成上下文,当标题无意义。
建议顺手补进跳过/排除列表(非阻断)。
验证记录
pnpm build:通过pnpm exec vitest run --project unit test/resumable-session-discovery.test.ts:31/31 通过(即 PR 自带用例全绿 —— 但如上,绿不代表覆盖真实 envelope)- 编译产物对
~/.codex/sessions下 755 条真实 rollout 跑discoverRolloutSessions(),并与 master 版(esbuild 转译同文件)逐会话 diff:坐实 38 条 botmux-origin 泄漏为本 PR 新引入 - 把真实泄漏 part 单独喂
isBotmuxInjectedPrompt()复现 guard 落空;对候选修复模式做 821 条命中 + 2 条外部讨论反例不误伤的验证
复核补充 — 收敛后的作者动作清单(REQUEST CHANGES 维持)经独立数据复核确认阻断项判定与修复方向,补充如下,供作者一次收口。 +48 条的精确构成(本机真实库,base 707 → head 755)
(此前 review 表格把后两类合并计为「外部会话 10」,精确拆分后为 7 真实 + 3 合成伪标题 —— 修好下述第 3 点后这 3 条即从 picker 消失。) 建议一次收口的三项
改完建议再对真实 rollout 库跑一遍 |
|
已按 review 修复并推送(commit
验证:
烦请复审。 |
deepcoldy
left a comment
There was a problem hiding this comment.
复审 (v2 · commit 1bbf297) — 阻断已解除,APPROVE(仍待授权合码)
作者按清单三项全部收口,我在本机真实数据上逐项复验通过。
逐项核对
1. 放宽 <botmux_routing> guard —— ✅ 正确实现
旧的严格模式(要求 </botmux_routing> 后紧接 <session_id>)已删除,替换为松结构锚定:^<botmux_routing>[\s\S]*?</botmux_routing>[\s\S]*?<user_message>[\s\S]*?</user_message>。旧严格模式是新模式的子集,删除不丢覆盖;注释也明确写了"匹配顺序边界而非维护白名单,避免新块插入时失效"。
2. 回归夹具换真实形态 —— ✅
既有 "drops botmux-origin … response_item" 用例的 envelope 已改为真实结构:<botmux_routing>…</botmux_routing> 后插入 <botmux_builtin_skills> + <identity>,并结束于 </user_message> 无 footer —— 正是 v1 假绿的那个形态,现在能真正命中。
3. 合成跳过列表 + 补测 —— ✅
^<codex_internal_context\b 与 ^<turn_aborted\b 已加入 ROLLOUT_SYNTHETIC_USER_PATTERNS,并新增两条"整条会话只有合成上下文 → 不进 picker"的用例。
真实数据复验(本机 755 条 codex rollout)
| v1(旧 head) | v2(本 commit) | |
|---|---|---|
| 发现会话数 | 755 | 562 |
| botmux-origin 泄漏 / 合成伪标题 | 190 🔴 | 0 ✅ |
| 相比 master 新浮出的真实外部会话 | 7 | 7 ✅(保留) |
v2 相比 master(707)少 152 条,全部是此前经 event_msg 路径泄漏的 botmux-origin 会话 —— 松模式把这批非本 PR 引入的 pre-existing 泄漏也一并修好了,它们本就不该出现在 picker,属正确收益。数字自洽:707 − 152 + 7 = 562。
额外健壮性检查
- ReDoS: 新模式含三个惰性量词,对 8 组针对性构造的 200KiB 对抗输入(未闭合标签、大量近似标签逼回溯、双边界交替片段等)逐一计时,全部 <1ms —— 惰性
[\s\S]*?+ 互异字面分隔符,线性无灾难回溯,安全。 - 误伤: "仅在正文讨论
<botmux_routing>/<user_message>"、代码块展示 envelope、tag 出现在中段(非^起始)等 —— 均正确 KEEP(不误 drop)。唯一理论误伤(prompt 字面以<botmux_routing>开头且含完整<user_message>)在持久化层与真实注入不可区分,符合既有"完整结构优先判 botmux"策略,可接受。 - 共享 guard 消费方:
isBotmuxInjectedPrompt亦被grok-transcript.ts复用;grok + adopt + session-adopt 等 8 个测试文件共 127/127 通过,无跨消费方回归。
验证记录
pnpm build:通过pnpm exec vitest run --project unit test/resumable-session-discovery.test.ts:33/33(新增 2 条 SYNTH 用例)- guard 全部消费方套件:127/127
- 编译产物对
~/.codex/sessions755 条真实 rollout 复跑 + 与 master 逐会话 diff:泄漏归零、真实外部会话保留 - 新模式 ReDoS 8 输入计时 + 误伤反例电池:全部符合预期
阻断项已彻底解除,方向与实现均正确。建议合并 —— 但按流程仍需授权后再合码。
deepcoldy
left a comment
There was a problem hiding this comment.
复审(v2 · commit 1bbf297)— REQUEST CHANGES:松正则存在可复现的超线性回溯
功能修复本身已验证正确:真实 rollout 结果为 562 条、7 条目标外部会话保留、152 条既有 botmux 泄漏移除,新增 synthetic 过滤和 127 个相关测试也都通过。误伤边界判断我认可。
但当前新增模式:
/^<botmux_routing>[\s\S]*?<\/botmux_routing>[\s\S]*?<user_message>[\s\S]*?<\/user_message>/并非线性。只要输入以 routing 开头,随后重复候选边界、但始终缺少最终 </user_message>,三个惰性量词会组合回溯:
const input = '<botmux_routing>'
+ '</botmux_routing><user_message>X'.repeat(n);
isBotmuxInjectedPrompt(input);在本 PR 导出的真实 guard 上实测:
| n | 输入大小 | 耗时 |
|---|---|---|
| 160 | 5,136 B | 11.1 ms |
| 320 | 10,256 B | 88.0 ms |
| 640 | 20,496 B | 697.4 ms |
输入翻倍约 8 倍耗时,呈立方级增长。这里不是不可达的纯理论输入:discovery 明确按完整 JSONL 行读取,仓库也专门支持单条大于 200KiB 的 prompt;一次 /adopt 会在 daemon 内同步执行该 guard,因此一个病态外部 rollout 足以长时间阻塞 daemon。此前“200KiB 全部 <1ms”的输入电池没有覆盖“重复精确 closing-routing + opening-user、缺 closing-user”这一组合。
建议不要继续用多段 wildcard regex,改为有序 startsWith + indexOf:
- 必须以
<botmux_routing>开头; - 从其后查找
</botmux_routing>; - 从 closing routing 之后查找
<user_message>; - 从 opening user 之后查找
</user_message>。
这个实现保持相同的结构语义且线性。我用该 helper 做了两组验证:
- 对当前真实库 14,237 个 event/response user text 与现正则逐一比对:两者均命中 1,518 个,0 个不一致;
- 对上述 204,816 B 病态输入:helper 返回 false,耗时约 0.25ms。
请替换这一个松正则,并增加该精确对抗形态的回归覆盖。其余 v2 改动无新增阻断。
验证记录:pnpm build 通过;8 个 guard/adopt 相关测试文件 127/127 通过;真实 rollout base 707 → head 562(新增 7、正确移除 152、伪标题 0)。未合码。
deepcoldy
left a comment
There was a problem hiding this comment.
复审更正 (v2 · commit 1bbf297) — 撤回上一条 APPROVE,改 REQUEST CHANGES
复核指出的正则性能阻断成立,我已独立复现坐实。我上一条 APPROVE 的 ReDoS 判断有漏 —— 此处更正。
🔴 P0 阻断:新增松正则存在灾难性回溯(可达 DoS)
新模式 ^<botmux_routing>[\s\S]*?</botmux_routing>[\s\S]*?<user_message>[\s\S]*?</user_message> 含两个相邻惰性量词,当中间分隔符成对完整重复但最终 </user_message> 缺失时组合回溯。我用真实 guard 复现:
输入 <botmux_routing> + </botmux_routing><user_message>X×n |
长度 | 耗时 |
|---|---|---|
| n=800 | ~25KB | 1.5s |
| n=1600 | ~51KB | 10.8s |
| n=3200 | ~100KB | 88s |
输入翻倍耗时约 ×8(组合回溯)。可达性坐实:forEachJsonLine 读完整行(无字节上限,注释明确"200KiB 单条记录也须整条解析"),guard 在 discoverRolloutSessions 里逐行调用 —— 磁盘上一条病态外部 rollout 就能让一次 /adopt 卡死几十秒(daemon 侧同步阻塞)。
我上一条为何漏: 我的对抗电池用的是近似片段(</botmux_routin 这类不闭合近似),没构造成对完整的中间分隔符重复——而后者才是让两个惰性量词乘积回溯的真正触发形态。教训记下。
修复:改成有序 startsWith + indexOf 线性扫描(不用三段 wildcard)
依次定位 routing 起点 → routing 终点 → user 起点 → user 终点,任一缺失即 false。已独立验证等价 + 免疫:
/** Structural check for a botmux routing envelope, done with ordered index
* scans instead of lazy `.*?` quantifiers — three chained lazy wildcards
* backtrack combinatorially on repeated complete delimiters with a missing
* final close (a disk rollout can be 200KiB+ and is scanned whole), so the
* regex form is a reachable DoS via /adopt. This is O(n). */
function matchesRoutingEnvelope(t: string): boolean {
if (!t.startsWith('<botmux_routing>')) return false;
const rc = t.indexOf('</botmux_routing>', '<botmux_routing>'.length);
if (rc < 0) return false;
const uo = t.indexOf('<user_message>', rc + '</botmux_routing>'.length);
if (uo < 0) return false;
return t.indexOf('</user_message>', uo + '<user_message>'.length) >= 0;
}把这个 helper 从 BOTMUX_INJECTION_PATTERNS 里拆出来,在 isBotmuxInjectedPrompt 里作为一条 || matchesRoutingEnvelope(text) 分支(其余正则保留)。
独立验证(与复核数据一致):
- 免疫: 同一 ~100KB 病态输入 0.099ms、~200KB 0.18ms(正则 88s → helper 亚毫秒);
- 等价: 本机 15,199 条真实 user text 上,正则与 helper 各命中 1,518,差异 0;
- 误伤: 现有反例(讨论 XML / 中段 tag / 无 user close)结果不变,真实 envelope(含 interposed skills / legacy shape)仍命中。
请作者补齐
- 用上面的线性
matchesRoutingEnvelope替换新增的松正则分支。 - 补一条对抗回归:
'<botmux_routing>' + '</botmux_routing><user_message>X'.repeat(n)(成对完整分隔符 + 缺尾闭合),断言在合理时限内返回 false —— 锁死这个形态不再退化。
功能结果与误伤边界我在 v2 上已确认无误(707→562、+7 真实外部、0 泄漏、0 合成伪标题),唯此性能项需收口。换成线性扫描 + 补回归后,再做一次最终复核即可。当前不应合码。
背景
Fixes #878.
Codex Desktop / CLI 0.147 的部分 rollout 不再写入
event_msg/user_message,真实用户输入只存在于response_item/message(role=user)。现有磁盘恢复扫描器因此无法生成标题,导致/adopt找不到 Codex 自身仍可恢复的 session。改动
event_msg/user_message标题路径。response_item的input_text作为文件结束时的 fallback。影响范围
仅修改 Codex/TRAE 磁盘 resumable session discovery;未改动 live backend、Claude-family、Antigravity、worker 或 IM 路径。现有 exclude、limit 与旧格式行为保持不变。
验证
pnpm build:通过pnpm exec vitest run --project unit test/resumable-session-discovery.test.ts:31/31 通过pnpm workflow-core:test:通过pnpm test:15,195 通过;31 项为无关环境依赖失败(系统 Git 2.20 不支持测试使用的git init -b、缺少 bwrap,以及 host/runtime 相关用例),相关 discovery 套件无失败